UNPKG

peasy

Version:

an easy but powerful parser

246 lines (158 loc) 15.7 kB
#Peasy :一个简单而强大的解析器 ###Parsing is easy! 有编写解析器的经历吗?有了Peasy ,再也不需要象以前那样玩高难的[抛球杂技](https://raw.github.com/chaosim/peasy/master/doc/ballacrobatics.jpg)了, 现在只需要[象这样](https://raw.github.com/chaosim/peasy/master/doc/dolphinball.jpg)就能把它轻松搞定:) ###**用法** **浏览器**: 复制twoside.js, peasy.js, logicpeasy.js(如果需要逻辑功能)到项目文件夹,按次序添加到yourpage.html的`<script>`标签。 **node.js**: 除了直接从github复制文件以外,node下也可以通过npm安装:`npm install peasy` **twoside**: twoside是我编写的一个小工具,使模块可以同时用于浏览器和node.js。如果不想使用它,直接忽略并删除peasy.js和logicpeasy.js前两行,并删除代码中的exports变量及其引用。 关于twoside的详细信息,请参阅https://github/chaosim/twoside。在无需考虑客户端浏览器的纯node环境下,可以忽略twoside,不影响使用。可以通过npm安装twoside:`npm install twoside` ###介绍 peasy从概念到实现以及使用都极其简单。peasy只包含一个类Parser,该类除了启动解析的parse成员函数以外,只包含两类成员函数,一类叫匹配函数,不带参数,其作用是直接检测被解析数据(data)和调整解析指针(cur),包括eoi, spaces, spaces1, digit, letter, upper, lower, number, string, identifier, identifierLetter, followIdentifierLetter等。另一类叫组合函数,产生匹配函数作为结果,包括rec, memo, orp, andp, notp, may, any, some, times, list, listn, follow, literal, char, wrap等。其中rec实现左递归功能,使得peasy具有最强的语法表达能力;memo实现缓存功能,尽可能将解析过程保持在最低的时间复杂度上。 logicpeasy 是对peasy的扩展,它使得解析规则可以用逻辑变量作为参数。 ###开始编写解析器 推荐如下方法: 新建模块(可以用samples下的某个文件作为模板),require peasy或logicpeasy,以peasy.Parser或logicpeasy.Parser作为基类,建立新Parser类。 建议编写代码以前,先阅读peasy本身的代码以及test和samples中的代码,这些代码都是简单自明的,读过之后您就能直接进入状态了。 为了让大家马上对用peasy编写解析器有个直观感觉,下面列出了samples/arithmatic.coffee的主体内容。为节省篇幅和便于阅读,代码中用...省略了不影响整体结构的段落。 exports.Parser = class Parser extends peasy.Parser constructor: -> super # call the constructor of peasy.Parser self = @ save 'this' to self in advance, avoid the problem that 'this' may be bound to wrong object. number = -> ... string = -> ... {orp, list, rec, memo, wrap, char, literal, spaces, eoi, identifier} = self question = char('?'); colon = char(':'); comma = char(','); dot = char('.') lpar = char('('); rpar = char(')') lbracket = char('['); rbracket = char(']') myop = (op) -> if op.length==1 then opFn = char(op) else opFn = literal(op) if _in_(op[0], identifierCharSet) -> spaces() and (op=opFn()) and spaces() and not _in_(data[self.cur], identifierCharSet) and ' '+op+' ' else -> spaces() and (op=opFn()) and spaces() and op posNeg = (op) -> ... positive = posNeg('+'); negative = posNeg('-') ... assign = myop('='); ..., bitorassign = myop('|=') error = (msg) -> throw self.data[self.cur-20..self.cur+20]+' '+self.cur+': '+msg expect = (fn, msg) -> fn() or error(msg) incDec = orp(inc, dec) prefixExpr = -> (op=incDec()) and (x=headExpr()) and op+x suffixExpr = -> (x=headExpr()) and (op=incDec()) and x+op paren = (item, left=lpar, right=rpar, msg='expect ) to match (') -> -> start=self.cur; left() and (x=item()) and expect(right, msg+' at: '+start) and x paren1 = paren(-> (spaces() and (x=expression()) and spaces() and x)) parenExpr = memo -> (x=paren1()) and '('+x+')' atom = memo orp(parenExpr, number, string, identifier) newExpr = -> new_() and (x=callProp()) and 'new '+x unaryTail = orp(prefixExpr, suffixExpr, atom) unaryExpr = -> (op=unaryOp()) and (x=unaryTail()) and op+x bracketExpr1 = wrap(paren(wrap(-> commaExpr()), lbracket, rbracket, 'expect ) to match (')) bracketExpr = -> (x=bracketExpr1()) and '['+x+']' wrapDot = wrap(dot) dotIdentifier = -> wrapDot() and (id=expect(identifier, 'expect identifier')) and '.'+id attr = orp(bracketExpr, dotIdentifier) param = paren -> (spaces() and (x=expression()) and spaces() and expect(rpar,'expect )')) or ' ' paramExpr = memo -> (x=param()) and '('+x+')' callPropTail = orp(paramExpr, attr) callPropExpr = rec -> (h=headExpr()) and (((e=callPropTail()) and h+e) or h) property = rec -> (h=headExpr()) and (((e=attr()) and h+e) or h) headAtom = memo orp(parenExpr, identifier) headExpr = memo orp(callPropExpr, headAtom) simpleExpr = memo orp(unaryExpr, prefixExpr, suffixExpr, callPropExpr, newExpr, atom) binaryOpPriorityMap = { 5: ['*', '/', '//', '%'], ...} binaryOpItems = [] do -> for k, ops of binaryOpPriorityMap for op in ops then binaryOpItems.push [op, {text:op, pri:parseInt(k)}] binarysm = new StateMachine(binaryOpItems) binaryOperator = memo -> m = binarysm.match(self.data, self.cur) if m[0] then self.cur = m[1]; m[0] expr = (n) -> binary = rec -> if x = binary() beforeOp = self.cur if (op=binaryOperator()) and (n>=op.pri>=x.pri) and (fn=expr(op.pri)) and y=fn() {text: x.text+op.text+y.text, pri:op.pri} else self.cur = beforeOp; x else if x=simpleExpr() then {text:x, pri:4} orBinary = expr(15) logicOrExpr = -> (x = orBinary()) and x.text wrapQuestion = wrap(question); wrapColon = wrap(colon) condition = -> (x=logicOrExpr()) and ((wrapQuestion() and (y=assignExpr()) and expect(wrapColon, 'expect :') and (z=assignExpr()) and x+'? '+y+': '+z) or x) assignOperator = orp(assign, addassign, ..., bitorassign) assignExpr_ = -> if (v=property()) and (op=assignOperator()) then (e = expect(assignExpr, 'expect the right hand side of assign.')) and v+op+e assignExpr = orp(assignExpr_, condition) expression_ = list(assignExpr, wrap(comma)) expression = -> x = expression_(); if x then x.join(',') @root = -> (x=expression()) and expect(eoi, 'expect end of input') and x 在samples/arithmatic2.coffee中,全部代码只有180行,代码又如此简单,能够完整处理javascript这种复杂程度的表达式,还包含了词法分析器的功能,可以说只有peasy能够做到。 上述代码有几点独特之处需要解释一下: * coffeescript的语法使得规则可读性非常好,不亚于其它任何编译器产生器的规则定义,javascript下则将稍逊一筹。 * 将所有规则在Parser类的constructor中设置为类的实例成员,而不是原型(prototype)成员,可以避免很多可能的与this绑定相关的问题。我曾经试过原型(prototype)成员的方案,测试和调试过程中发现有很多隐患。另外,避免查找prototype继承链,也能提高解析器的速度。 * 关于二元表达式的处理(expr(n)和它生成的binary = rec ...)也是peasy的独特之处。这将在下文中进一步解释。 也可以从peasy.coffee(或js)或logicpeasy.coffee(或js)开始,直接在其中进行修改直到完成自己的解析器。 ### 实例介绍 项目中带有几个实例,解说一下: dsl.coffee 是我在另一项目中利用peasy解析小语言的实例。 arimatic.coffee利用peasy对左递归的支持完成了javascript表达式的解析。它的特点是用一个函数生成所有优先级二元表达式的左递归规则。解析的时候从最低优先级的规则(逗号表达式)开始,调用栈经历所有优先级后得到最终结果。这种方法是线性时间的,但因为要经历很长的调用栈,对效率会有所影响。比如即使象1这样的表达式,也要变换成乘法式,加法式,...,逻辑或表达式,条件式,逗号式。 arithmatic2.coffee避免了上述问题,可以根据运算符跨越式提升表达式优先级。比如 1 || 1,可以直接将1提升成或表达式的优先级。 这两个模块演示的算术表达式的解析方法都是独特的,我尚没有发现其它类似的实现。如果您发现有相近或更好的方法,请告诉我。 ### api ####class Parser(peasy.coffee/peasy.js): 解析器基类 #####数据成员 `data`:被解析数据,常见数据类型是文本,字符串。也可以用peasy解析二进制流,数组,列表,树等其他数据 `cur`:解析指针,指向当前解析位置 `ruleIndex`: 每一个rec和memo的匹配函数(rule参数)都对应唯一的一个ruleIndex `ruleStack`: 左递归规则(rec(rule))执行的时候需要查看ruleStack,请参看rec的代码 `cache`: 保存左递归规则(rec(rule))和缓存规则(memo(rule))已经解析到的结果 ##### 成员函数 以下介绍peasy的组合函数和匹配函数。这里只列举最常用的几个,因为所有的函数都很简单,函数名都是自我解释性的,而且彼此平行,都可以根据需要使用、删除,修改。把它们包含在peasy中,主要起到示范的作用,如果需要用到,请参看实现代码。 以下说明中,成功指返回真值,包括非0数字,非空字符串,数组,对象等。失败指返回假值,包括undefined, null, 0, ''等。 `parse(data, root=self.root, cur=0)`, 设置被解析数据data,起始匹配函数(默认为self.root), 开始解析指针(默认为0). #####组合函数 `rec(rule)`, 将rule作为左递归规则处理。产生的结果函数将反复查看缓存,获取初始解析结果,并一直扩展到没有更多的结果为止。 `memo(rule)`,产生的结果函数将查看并缓存rule在每一个位置的解析结果,避免重复解析。 `orp(items...)`。产生的匹配函数依次从同一开始位置执行items中的项,当某一项解析成功则返回该项结果表示整体成功,所有项失败才失败。 orp可能是最常用到的组合函数。因为不能够写`item1() or item2() ... or itemn()`, 而必须写`start=self.cur; items() or ((self.cur=start) or item2()) ... or ((self.cur=start) or itemn())`。如果忽视这一点,这将会成为代码中的一个陷阱。 `andp(items...)`, 产生的匹配函数依次执行items中的项,每一次都从前一项的到达位置继续执行,当某一项解析失败则立即返回undefined并表示整体失败,所有项成功才成功并返回最后一项的结果。 andp在实际的代码中很少用到。因为可以写`item1() and item2() ... and itemn()`。 而且经常需要这样写:` (x=item1()) and (y=item2()) ... itemn() and doSomeProcess(x, y)` `literal: literal(item)`, item是字符串。产生的匹配函数将判断以解析指针开始的字符串是否与item匹配。如果匹配则成功,否则失败。 出于方便,其它组合函数会根据需要将它们的字符串类型的参数x转换为literal(x)。请根据源码查看哪些函数用到了literal转换。 `char(c)`, c是字符。产生的匹配函数将判断以解析指针开始的字符是否为c。如果匹配则成功,否则失败。 `wrap(item, left=spaces, right=spaces)` 产生的匹配函数执行如下判断`if left() and result = item() and right() then result` #####匹配函数 `eoi`: 如果遇到数据结束(end of input), 则成功。 `spaces`0个或多个空格。总是成功,结果为(空格个数+1)。 `spaces1`: 1个或多个空格。结果为空格个数。如果空格个数为0,则失败。 ####logicpeasy的Parser类 类成员 `trail``self.trail = new Trail`, 保存了变量绑定踪迹。 #####非正规匹配函数 *前面介绍的匹配函数是正规的匹配函数,可以作为象orp这样的组合函数的参数。非正规匹配函数和匹配函数类似,也直接操作解析数据和指针。但是因为带有参数,因而不能直接作为orp之类组合函数的参数,如果必要,可以转换为 `-> fn(arg1, arg2, ...)`。* `bind(vari, term)`: 绑定变量和项 `unify(x, y, equal=((x, y) -> x==y))`: 以equal作为相等函数,合一xy `unifyList = (xs, ys, equal=((x, y) -> x==y))`: 以equal作为相等函数,合一两个列表xy #####组合函数 `unifyChar(x)``unifyDigit(x)`, `unifyLetter(x)`, `unifyLower(x)`, `unifyUpper(x)`, `unifyIdentifier(x)`:这些函数与peasy中的类似名称的函数功能相近,所不同之处是参数x可以是逻辑变量。 `orp(items...)`: 重载的orp扩展了与逻辑变量有关的trail处理。 #####逻辑变量与可以进行合一的数据类型 Var,vars,Dummy, dummy用来构造逻辑变量。 UObject, uobject, UArray, uarray, Cons, cons, unifiable用来构造可以包含逻辑变量并合一的值。 ### 0.3.2 新功能 * 不再用gruntjs,改用gulpjs * 用gulp-twoside.js帮助为浏览器进行模块化封装 * 增加lineparser以支持行号和列号 ###peasy项目的由来 peasy项目是以本人的下一代编程语言项目dao(python)和daonode(coffeescript/javascript)的解析特性为基础移植而来的。dao项目实现了逻辑编程范式和函数编程范式的自然结合,是lisp和prolog语言的合体,并且内置包含了解析器功能。以逻辑变量作为参数编写文法规是dao项目的独创,这种逻辑参数文法具有强大的能力,具有超越传统编译原理所基于的乔姆斯基文法(包括上下文无关文法cfg、上下文相关文法csg以及0型文法)以及现在流行的解析表达式文法(peg)的表达能力(有兴趣的可以查阅编译原理中有关0-4型文法表达能力的一些论述,[比如这个链接](http://ccl.pku.edu.cn/doubtfire/Syntax/Introduction/Chomsky/Chomsky_Hierarchy/Chapter%2024%20The%20Chomsky%20Hierarchy.htm)),同时无损于解析算法的时间复杂度。 ### python实现 本项目有peasy的老版本python实现,因为时间问题,没有与coffee/js版本同步更新。 ### 关于Peasy的其它信息 **更多文档**:http://chaosim.github.io/peasy/ 有更多关于peasy的文档。 **项目站点**:github <https://github.com/chaosim/peasy> 。 **测试** : Peasy使用karma/jasmine和mocha/chai进行测试框架,请看文件夹“/test” **报告bug** :要报告或查找bug,请转到<https://github.com/chaosim/peasy/issues> ,或电邮至simeon.chaos @ gmail.com **开发平台** : Windows 7, node.js的0.10.0, CoffeeScript的1.6.3 。 **许可证(license)** :MIT,请看LICENSE